//==============================================================================
//
// C++ Minimalistic-Control-Library for the Pappradio V 2.0
//
// Copyright (C) 2011  Stefan Fendt
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the  GNU Lesser General Public License  as published by the Free
// Software Foundation;  either version 2.1 of the License,  or (at your option)
// any later version.
//
// This library is distributed  in the hope that it will be useful,  but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
// details.
//
// You  should have  received  a copy  of the  GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//
// =============================================================================


// =============================================================================
// include what we need ...
// =============================================================================

#include "pappradio.hpp"
#include "hidapi.h"
#include <iso646.h>

// =============================================================================
// CONSTRUCTOR: Set all required defaults, initialize everything...
// =============================================================================

Pappradio::Pappradio()
{
    // set device status to not opened
    m_DeviceHandle = 0;
    m_DeviceReady  = false;

    // for security reasons clear dataword
    for( int n=0; n<32; ++n )
    {
        m_Dataword.byte[n] = 0;
    }

    // there is no loaded fqdata, for now...
    m_FQDataAvailable = false;
}

// =============================================================================
// DESTRUCTOR: Heeeelp,... we're going to die...
// =============================================================================

Pappradio::~Pappradio()
{
    // check if someone forgot closing it *sigh* and if so, close it...
    close();
}

// =============================================================================
// Is everything open/OK? This exists just for convenience, so we do not have
// to type this all and all over again...
// =============================================================================

bool Pappradio::isOpen()
{
    // check if it's still closed...
    if( ( not m_DeviceHandle ) or
        ( not m_DeviceReady  ) )
    {
        return( false );
    }

    // no,... it _is_ open,...
    return( true );
}

// =============================================================================
// Try to detect and open the radio-hardware
// =============================================================================

enum Pappradio::ERRORS Pappradio::open()
{
    // check if there is an FQData.DAT loaded... if not, return with an error
    if( not m_FQDataAvailable )
    {
        return( ERR_INIT_FQDATA );
    }

    // ok, let's try to open the pappradio via Vendor-ID and Product-ID
    m_DeviceHandle = hid_open( m_USBVendorID, m_USBProductID, 0 );
    
    if( m_DeviceHandle )
    {
        // ok, there is something, which could be a pappradio... check this!
        
    	int result;
    	wchar_t manufacturerString[256];
    	wchar_t productString[256];
    	
        result = hid_get_manufacturer_string( (hid_device*)m_DeviceHandle, 
                                              manufacturerString, 
                                              256 );
        
        result = hid_get_product_string( (hid_device*)m_DeviceHandle, 
                                         productString, 
                                         256 );
        
        // check if it really is a pappradio. This is necessary because the 
        // pappradio does not have a unique Product/Manufacturer-ID...
        
        if( wcsncmp( productString,      L"HCJB PAPPRADIO", 14 ) != 0 ||
            wcsncmp( manufacturerString, L"HCJB"          ,  4 ) != 0 )
        {
            // NOTE: it might be more clever to get a list of all USB-Devices
            //       connected to the system with the above USB-IDs and then
            //       checking each one if it is a pappradio or not...
            
            // close device
            hid_close( (hid_device*)m_DeviceHandle );
            
            // zero device-handle
            m_DeviceHandle = 0;
            
            // return with error
            return( ERR_PRODUCT_MISMATCH );
        }
        else
        {
            // OK, everything seems to have worked... flag device ready...
        	m_DeviceReady = true;
        }
    }
    else
    {
        // no matching device found...
        return( ERR_NOT_FOUND );
    }

    // if we reach this point, everything is set up and ready to go...
    return( ERR_NONE );
}

// =============================================================================
// Do not try but just close the communication to the radio-hardware
// =============================================================================

void Pappradio::close()
{
    // if it's open, then close it and release libusb
    if( isOpen() )
    {
        // release device...
        hid_close( (hid_device*)m_DeviceHandle );
        
        //libusb_release_interface    ( m_DeviceHandle, 0 );
    	//libusb_close                ( m_DeviceHandle    );

        // shut down libusb
        //libusb_exit(0);

        m_DeviceHandle = 0;
        m_DeviceReady  = false;
    }
}

// =============================================================================
// Set a new LO-frequency and transmit it to the radio-hardware. As the hardware
// maybe does not permit to get an exact matching frequency it is generally ad-
// vised to call getFrequency() afterwards to see the real LO-frequency reached.
// =============================================================================

void Pappradio::setFrequency( double frequency )
{
    m_TargetFrequency = frequency;

    // caluculate line index from frequency
    int lineIndex = (int)floor( ( m_TargetFrequency / 1000.0 ) + 0.5f ) - 1;

    if( lineIndex < 0 )
    {
        lineIndex = 0;
    }
    else
    if( lineIndex > 29999 )
    {
        lineIndex = 29999;
    }

    for( int n = 0; n <= 16; n++ )
    {
        int m = 16-n;
        m_Dataword.icsWord[m] = m_FQData[lineIndex].byte[n];
    }

    // calculate the real frequency the LO is running on this is done by taking
    // the base-clock of the ICS (in this case it's 10.0 MHz) and apply the
    // real dividers and multipliers.

    // ------------------------------------------------------------------------
    // first get the input-divider out of the programming-word...
    // ------------------------------------------------------------------------
    int imode  = 0;
    int iDiv   = 0;

    imode  = (getBitInProgWord(  0, m_FQData[lineIndex].byte ) <<  0)+
             (getBitInProgWord(  1, m_FQData[lineIndex].byte ) <<  1);

    iDiv   = (getBitInProgWord(  2, m_FQData[lineIndex].byte ) <<  0)+
             (getBitInProgWord(  3, m_FQData[lineIndex].byte ) <<  1)+
             (getBitInProgWord(  4, m_FQData[lineIndex].byte ) <<  2)+
             (getBitInProgWord(  5, m_FQData[lineIndex].byte ) <<  3)+
             (getBitInProgWord(  6, m_FQData[lineIndex].byte ) <<  4)+
             (getBitInProgWord(  7, m_FQData[lineIndex].byte ) <<  5)+
             (getBitInProgWord(  8, m_FQData[lineIndex].byte ) <<  6)+
             (getBitInProgWord(  9, m_FQData[lineIndex].byte ) <<  7)+
             (getBitInProgWord( 10, m_FQData[lineIndex].byte ) <<  8)+
             (getBitInProgWord( 11, m_FQData[lineIndex].byte ) <<  9)+
             (getBitInProgWord( 12, m_FQData[lineIndex].byte ) << 10);

    switch( imode )
    {
        case 0:
            iDiv = 1;
            break;
        case 1:
            iDiv = 2;
            break;
        case 2:
            iDiv ^= 0xFFFFFFFF;
            iDiv &= 0x0F;
            iDiv += 2;
            break;
        case 3:
            iDiv &= 0x03FF;
            iDiv += 8;
            break;
    }

    // This was easy, wasn't it? ;-)

    // ------------------------------------------------------------------------
    // ... now check for the VCO-divider (which is a multiplier, actually...)
    // ------------------------------------------------------------------------

    int vDiv   = 0;

    vDiv   = (getBitInProgWord( 13, m_FQData[lineIndex].byte ) <<  0)+
             (getBitInProgWord( 14, m_FQData[lineIndex].byte ) <<  1)+
             (getBitInProgWord( 15, m_FQData[lineIndex].byte ) <<  2)+
             (getBitInProgWord( 16, m_FQData[lineIndex].byte ) <<  3)+
             (getBitInProgWord( 17, m_FQData[lineIndex].byte ) <<  4)+
             (getBitInProgWord( 18, m_FQData[lineIndex].byte ) <<  5)+
             (getBitInProgWord( 19, m_FQData[lineIndex].byte ) <<  6)+
             (getBitInProgWord( 20, m_FQData[lineIndex].byte ) <<  7)+
             (getBitInProgWord( 21, m_FQData[lineIndex].byte ) <<  8)+
             (getBitInProgWord( 22, m_FQData[lineIndex].byte ) <<  9)+
             (getBitInProgWord( 23, m_FQData[lineIndex].byte ) << 10)+
             8;

    // ----------------------------------------------------------------
    // check Out divider... now things get really awkward...
    // ----------------------------------------------------------------
    int oData  = 0;
    int oDiv   = 0;
    int expo   = 0;
    oData  = (getBitInProgWord(  95, m_FQData[lineIndex].byte ) <<  0)+
             (getBitInProgWord(  96, m_FQData[lineIndex].byte ) <<  1)+
             (getBitInProgWord(  97, m_FQData[lineIndex].byte ) <<  2)+
             (getBitInProgWord(  98, m_FQData[lineIndex].byte ) <<  3)+
             (getBitInProgWord(  99, m_FQData[lineIndex].byte ) <<  4)+
             (getBitInProgWord( 100, m_FQData[lineIndex].byte ) <<  5)+
             (getBitInProgWord( 101, m_FQData[lineIndex].byte ) <<  6)+
             (getBitInProgWord( 102, m_FQData[lineIndex].byte ) <<  7)+
             (getBitInProgWord( 103, m_FQData[lineIndex].byte ) <<  8)+
             (getBitInProgWord( 104, m_FQData[lineIndex].byte ) <<  9)+
             (getBitInProgWord( 105, m_FQData[lineIndex].byte ) << 10)+
             (getBitInProgWord( 106, m_FQData[lineIndex].byte ) << 11)+
             (getBitInProgWord( 107, m_FQData[lineIndex].byte ) << 12)+
             (getBitInProgWord( 108, m_FQData[lineIndex].byte ) << 13)+
             (getBitInProgWord( 109, m_FQData[lineIndex].byte ) << 14);

    if( ( oData & 0x7FFF ) ==   0 ) oDiv =  2;
    else
    if( ( oData & 0x7FFF ) ==   1 ) oDiv =  3;
    else
    if( ( oData & 0x7FFF ) ==   8 ) oDiv =  4;
    else
    if( ( oData & 0x7FFF ) ==   2 ) oDiv =  5;
    else
    if( ( oData & 0x7FFF ) ==   9 ) oDiv =  6;
    else
    if( ( oData & 0x7FFF ) ==   3 ) oDiv =  7;
    else
    if( ( oData & 0x7FFF ) == 236 ) oDiv =  8;
    else
    if( ( oData & 0x7FFF ) ==  11 ) oDiv =  9;
    else
    if( ( oData & 0x7FFF ) == 220 ) oDiv = 10;
    else
    if( ( oData & 0x7FFF ) ==  19 ) oDiv = 11;
    else
    if( ( oData & 0x7FFF ) == 204 ) oDiv = 12;
    else
    if( ( oData & 0x7FFF ) ==  27 ) oDiv = 13;
    else
    if( ( oData & 0x07 ) == 4 )
    {
       oDiv = ((~( oData >> 3 )) & 31 )+6;
    }
    else
    if( ( oData & 0x07 ) == 5 )
    {
        expo = ((((oData)>>5)&1)<<1) +
               ((((oData)>>4)&1)   );

       oDiv   = ((( oData >> 6 ) + 3 )*2)+((~(oData>>3))&1);
       oDiv <<= expo;
    }

    if( oDiv != 0 && iDiv != 0 && vDiv != 0 )
    {
        m_RealFrequency = (10000000.0/4.0) * (double)vDiv /
                          ( (double)iDiv * (double)oDiv );
    }

    sendData( true );
}

// =============================================================================
// Return current _real_ LO-frequency.
// =============================================================================

double Pappradio::getFrequency()
{
    return( m_RealFrequency );
}

// =============================================================================
// Set the build-in bandpass (preselector) to either OFF or some allowed value
// =============================================================================

void Pappradio::setFilter( enum FILTERS filter )
{
    m_Filter = filter;

    // just set the appropriate bits in the dataword
    switch( m_Filter )
    {
        case NO_FILTER:
        {
            m_Dataword.bitBP0 = 1;
            m_Dataword.bitBP1 = 1;
            m_Dataword.bitBP2 = 0;
            break;
        }

        case BP_02400_04000:
        {
            m_Dataword.bitBP0 = 0;
            m_Dataword.bitBP1 = 0;
            m_Dataword.bitBP2 = 1;
            break;
        }

        case BP_04000_07500:
        {
            m_Dataword.bitBP0 = 1;
            m_Dataword.bitBP1 = 0;
            m_Dataword.bitBP2 = 1;
            break;
        }

        case BP_07500_14000:
        {
            m_Dataword.bitBP0 = 0;
            m_Dataword.bitBP1 = 1;
            m_Dataword.bitBP2 = 1;
            break;
        }

        case BP_14000_30000:
        {
            m_Dataword.bitBP0 = 1;
            m_Dataword.bitBP1 = 1;
            m_Dataword.bitBP2 = 1;
            break;
        }

        case EXT_1:
        {
            m_Dataword.bitBP0 = 0;
            m_Dataword.bitBP1 = 0;
            m_Dataword.bitBP2 = 0;
            break;
        }

        case EXT_2:
        {
            m_Dataword.bitBP0 = 1;
            m_Dataword.bitBP1 = 0;
            m_Dataword.bitBP2 = 0;
            break;
        }

        case EXT_3:
        {
            m_Dataword.bitBP0 = 0;
            m_Dataword.bitBP1 = 1;
            m_Dataword.bitBP2 = 0;
            break;
        }
    }
    sendData( false );
}

// =============================================================================
// Get back the current bandpass setting...
// =============================================================================

enum Pappradio::FILTERS Pappradio::getFilter()
{
    return( m_Filter );
}

// =============================================================================
// Set the attenuator to either 0, -10, -20, -30 dB
// =============================================================================

void Pappradio::setAttenuator( enum ATTENUATORS attenuator )
{
    m_Attenuator = attenuator;

    switch( m_Attenuator )
    {
        case ATT00:
        {
            m_Dataword.bitAT0 = 0;
            m_Dataword.bitAT1 = 0;
            break;
        }
        case ATT10:
        {
            m_Dataword.bitAT0 = 1;
            m_Dataword.bitAT1 = 0;
            break;
        }
        case ATT20:
        {
            m_Dataword.bitAT0 = 0;
            m_Dataword.bitAT1 = 1;
            break;
        }
        case ATT30:
        {
            m_Dataword.bitAT0 = 1;
            m_Dataword.bitAT1 = 1;
            break;
        }
    }
    sendData( false );
}

// =============================================================================
// Get back current attenuator-setting
// =============================================================================

enum Pappradio::ATTENUATORS Pappradio::getAttenuator()
{
    return( m_Attenuator );
}

// =============================================================================
// Load "fqdata.dat"
//
// DO THIS BEFORE trying to open the device!
// =============================================================================

bool Pappradio::loadFQData( std::string filename )
{
    std::cerr << "FQData.DAT-Filename: \""
              << filename
              << "\"\n";

    std::fstream fqdatafile( filename.c_str(), std::ios::binary|std::ios::in );

    if( fqdatafile.is_open() )
    {
        int lineIndex=0;        // index into fqdata-"lines" in memory
        char textline[1024];    // 1024 chars is really enough for one line

        while( not fqdatafile.eof() )
        {
            fqdatafile.getline( textline, 1024 );

            // OK, we just take this line we read in as a hex-word if it
            //
            // a) has a size of _exactly_ 33 nibbles _and_
            // b) only consists of the characters 0-9, a-f and/or A-F
            //

            // check number of nibbles and content

            int  i = 0;
            bool isHexWord = true;

            while( i < 1024            and // range-check first!
                   textline[i] != '\n' and
                   textline[i] != '\r' and
                   textline[i] != '\0' and
                   isHexWord   == true )
            {
                // test for pure hexnibble-content
                if( ( textline[i] >= '0' and textline[i] <= '9' ) or
                    ( textline[i] >= 'a' and textline[i] <= 'f' ) or
                    ( textline[i] >= 'A' and textline[i] <= 'F' ) )
                {
                    // nothing in this case...
                }
                else
                {
                    isHexWord = false;
                }

                // test for length (-1!)
                if( i > 32 )
                {
                    isHexWord = false;
                }

                i++; // go to next character...
            }

            if( isHexWord and lineIndex < 30000 )
            {
                for( int n=0; n<33; n++ )
                {
                    unsigned char currentNibble = textline[n];

                    if( currentNibble >= '0' and
                        currentNibble <= '9' )
                    {
                        currentNibble -= '0';
                    }
                    else if( currentNibble >= 'a' and
                             currentNibble <= 'f' )
                    {
                        currentNibble -= 'a';
                        currentNibble += 10;
                    }
                    else if( currentNibble >= 'A' and
                             currentNibble <= 'F' )
                    {
                        currentNibble -= 'A';
                        currentNibble += 10;
                    }

                    int addressIndex = (n+1)/2;
                    int nibbleIndex  = (n+1)&1;

                    if( nibbleIndex == 0 )
                    {
                        // modify high nibble
                        unsigned char value =
                            m_FQData[lineIndex].byte[addressIndex];

                        // mask out high nibble
                        value &= 0x0f;

                        // or current nibble in
                        value |= currentNibble << 4;

                        // store
                        m_FQData[lineIndex].byte[addressIndex] = value;
                    }
                    else
                    {
                        // modify low nibble
                        unsigned char value =
                            m_FQData[lineIndex].byte[addressIndex];

                        // mask out low nibble
                        value &= 0xf0;

                        // or current nibble in
                        value |= currentNibble;

                        // store
                        m_FQData[lineIndex].byte[addressIndex] = value;
                    }
                }

                // increment line-index
                lineIndex++;
            }
        }

        m_FQDataAvailable = true;
        fqdatafile.close();
    }
    else
    {
        // uhh... this should not happen... file does not exist...
        return( false );
    }

    return( true );
}

// =============================================================================
// Send data. Just used internally...
// =============================================================================

bool Pappradio::sendData(bool freqChange)
{
    m_Dataword._PRG_FW_BIT_ = 0;    // never send Firmware-Boot-Bit...
                                    // !!!DANGEROUS!!!
                                    //
    m_Dataword.bitCHK0      = 0;    // needs to be set to 0 acc. to spec
                                    //
    m_Dataword.bitCHK1      = 1;    // needs to be set to 1 acc. to spec
                                    //
    m_Dataword.clockOut     = 1;    // needs to be set to 1 for all
                                    // non-pre-series radios. As far as I know
                                    // none of them were sold. Maybe this needs
                                    // a control-function.
                                    //
    m_Dataword.freqChange   = freqChange? 1:0;
                                    // flag a frequency change or maybe not
                                    //
    m_Dataword.activePLL    = 0;    // use ICS 307-03 instead of SI570
                                    // currently a version with SI570 does not
                                    // exist.

    if( isOpen() )
    {
        int const   TIMEOUT_MS                  = 5000;
	    int const   ENDPOINT_INT_OUT            = 0x02;

	    int         sentBytes                   = 0;

        unsigned char outbuffer[sizeof(m_Dataword)+1];
        
        outbuffer[0] = 0; // report-number has to be zero for the pappradio...
        
        // copy data into output-buffer
        for( int n=0; n<sizeof(m_Dataword); ++n )
        {
            outbuffer[1+n] = m_Dataword.byte[n];
        }

        // transfer data to pappradio
        int result = hid_write( (hid_device*)m_DeviceHandle, 
                                outbuffer, 
                                sizeof(m_Dataword)+1 );

        if( result != (sizeof(m_Dataword)+1) )
        {
            // oops... this should not happen...
            return( false );
        }
    }

    return( true );
}

// =============================================================================
// Get a bit out of the ICS-Programming-Word. Just used internally...
// =============================================================================

bool Pappradio::getBitInProgWord( unsigned int  bitNumber,
                                  unsigned char progWord[17] )
{
    unsigned int const byteOffset = 16 - bitNumber / 8;
    unsigned int const bitOffset  = bitNumber % 8;

    if( progWord[byteOffset] & (1<<bitOffset) )
    {
        return( true );
    }
    else
    {
        return( false );
    }
}
